/**
 * 
 */
package ac.technion.StreamMania.connectivity;

import ac.technion.StreamMania.connectivity.iface.INetworkListener;
import ac.technion.gcchat.messages.Introduction;
import ac.technion.gcchat.messages.MessageType;
import ac.technion.gcchat.messages.NewStreamMessage;
import ac.technion.gcchat.messages.SerializationAgents;
import ac.technion.gcframework.layers.AbstractApplicationLayer;
import ac.technion.gcframework.membership.InconsistentViewChangeEvent;
import ac.technion.gcframework.membership.Member;
import ac.technion.gcframework.serialization.Data;
import ac.technion.gcframework.serialization.IObjectSerializationAgent;

/**
 * This is the top-most layer of our protocol stack that will be used in media streaming application. 
 * There are 8 types of messages to be handles during the streaming session.
 * 
 * @author Arnony
 * 
 */
public class NetworkAbstractionAppLayer extends AbstractApplicationLayer
{
	/* From MessageType.xsd */
	private static final byte MT_INTRODUCTION = 1;		//New person joins the streaming session.
	private static final byte MT_LEAVE_NOTIFICATION = 2;//A person leaves the streaming session.
	private static final byte MT_NEW_STREAM = 3;		//The sender notifies that a new stream is about to begin.
	private static final byte MT_STREAMING = 4;			//A data file is sent to receivers (a single chunk)
	private static final byte MT_PAUSE = 5;				//The sender asks the receivers to pause the the streaming.
	private static final byte MT_CONTINUE = 6;			//The sender allows the receivers to continue playing.
	private static final byte MT_STOPPED = 7;			//The sender terminates the current streaming session.
	private static final byte MT_STREAM_FINISHED = 8;	//The sender notifies that he's done sending the file.
	
	/* Here we do serialization of the 3 XML schemas available at core/schemas */
	private static final IObjectSerializationAgent s_messageTypeSerializationAgent = SerializationAgents.getAgent(MessageType.class);
	private static final IObjectSerializationAgent s_introductionSerializationAgent = SerializationAgents.getAgent(Introduction.class);
	private static final IObjectSerializationAgent s_NewStreamSerializationAgent = SerializationAgents.getAgent(NewStreamMessage.class);
	
	private final String m_strName;
	private final boolean m_isSender;
	private final INetworkListener m_networkListener;
	
	/**
	 * The class constructor.
	 * @param name - name of the current user.
	 * @param networkListener - the object for handling network operations.
	 * @param isSender - True iff the current user is a sender.
	 */
	public NetworkAbstractionAppLayer(String name, INetworkListener networkListener, boolean isSender)
	{
		m_strName = name;
		m_networkListener = networkListener;
		m_isSender = isSender;
	}
	
	/**
	 * The function retrieves the message type and calls the relevant method.
	 * @param member is the current member.
	 * @param data is the received data.
	 */
	protected void receiveData(Member member, Data data)
	{
		MessageType mType = extractMessageType(data);		
		handleMessageByType(mType.getType(), member, data);
	}
	
	/**
	 * The function receives the message type (1-8) and runs the appropriate method. 
	 * @param type is the message type (1-8)
	 * @param member is the current member.
	 * @param data is the received data.
	 */
	private void handleMessageByType(byte type, Member member, Data data)
	{
		switch (type)
		{

			case MT_INTRODUCTION:
				receiveIntroduction(member, data);
				break;
			case MT_LEAVE_NOTIFICATION:
				handleLeaveNotification(member);
				break;
			case MT_NEW_STREAM:
				handleNewStream(member, data);
				break;
			case MT_STREAMING:
				receiveStreamingBlock(member, data);
				break;
			case MT_PAUSE:
				m_networkListener.handlePause();
				break;
			case MT_CONTINUE:
				m_networkListener.handleContinue();
				break;
			case MT_STOPPED:
				m_networkListener.handleSenderStopped();
				//introduceToAll();
				break;
			case MT_STREAM_FINISHED:
				m_networkListener.handleSenderFinished();
				//introduceToAll();
				break;

				
		}
	}
	
	int counter = 0;
	/**
	 * The function is called when a new chunk of the media file is received.
	 * @param member is the current member.
	 * @param data is the received data (not including types & introduction. 
	 */
	private void receiveStreamingBlock(Member member, Data data)
	{
		m_networkListener.handleReceivedMedia(data.copyOutData(), counter++);
	}
	
	/**
	 * The function is called when the sender announced that a new streaming session opens.
	 * @param member is the current member.
	 * @param data is the data containing information on the new session.
	 */
	private void handleNewStream(Member member, Data data) 
	{
		NewStreamMessage message = extractNewStreamMessage(data);
		m_networkListener.handleNewStream(message.getFileName(), message.getLengthInKb(), message.getLengthInSeconds());
	}
	
	/**
	 * The function is called when a member has left the streaming session.
	 * @param member is the leaving member.
	 */
	private void handleLeaveNotification(Member member)
	{
		m_networkListener.memberLeft(member);
	}

	/**
	 * The function extracts the message type (1-8) from the MessageType schema.
	 * @param data is the received data.
	 */
	private MessageType extractMessageType(Data data)
	{
		return (MessageType)data.extractHeader(s_messageTypeSerializationAgent);
	}
	
	/**
	 * The function integrates the message type with the data object.
	 * @param data is the data object.
	 * @param type is the message type to be integrated. 
	 */
	private void integrateMessageType(Data data, byte type)
	{
		MessageType messageType = new MessageType();
		messageType.setType(type);
		data.integrateHeader(messageType, s_messageTypeSerializationAgent);
	}
	
	/**
	 * The function extracts a new stream message from the received data.
	 * @param data is the received data.
	 * @return the message itself.
	 */
	private NewStreamMessage extractNewStreamMessage(Data data) {
		return (NewStreamMessage)data.extractHeader(s_NewStreamSerializationAgent);
	}
	
	/**
	 * The function extracts the introduction info from the data object.
	 */
	private Introduction extractIntroduction(Data data)
	{
		return (Introduction)data.extractHeader(s_introductionSerializationAgent);
	}
	
	/**
	 * The function integrates the introduction info with the data object.
	 * @param data is the relevant data object. 
	 */
	private void integrateIntroduction(Data data)
	{
		Introduction introduction = new Introduction();
		introduction.setName(m_strName);
		introduction.setIsSender(m_isSender);
		data.integrateHeader(introduction, s_introductionSerializationAgent);
	}

	/**
	 * The function receives an introduction message and runs relevant method.
	 * @param member is the member who sent the introduction.
	 * @param data is the data object sent by the member.
	 */
	private void receiveIntroduction(Member member, Data data)
	{
		Introduction introduction = extractIntroduction(data);
		m_networkListener.memberJoined(member, introduction.getName(), introduction.isIsSender());
	}
	
	public void introduceToAll()
	{
		Data data = new Data();
		integrateIntroduction(data);
		integrateMessageType(data, MT_INTRODUCTION);
		sendData(data);	
	}
	
	/** 
	 * The function introduces us to the user by sending him an introduction message.
	 * @param joiner is the new joining person. 
	 */
	private void introduceTo(Member joiner)
	{
		Data data = new Data();
		integrateIntroduction(data);
		integrateMessageType(data, MT_INTRODUCTION);
		sendDataTo(joiner, data);	//Framework's function
	}

	/** 
	 * The function makes a list of members reconnected \ disconnected and runs the relevant
	 * methods of INetworkListener. Also introduces the user to all members that
	 * joined the chat. 
	 */
	protected void handleViewChange(InconsistentViewChangeEvent viewChangeEvent)
	{
		Member[] joiners = viewChangeEvent.getJoiners();
		for (int i = 0; i < joiners.length; i++)
		{
			Member joiner = joiners[i];
			m_networkListener.memberReConnected(joiner);
		}
		
		Member[] leavers = viewChangeEvent.getLeavers();
		for (int i = 0; i < leavers.length; i++)
		{
			Member leaver = leavers[i];
			m_networkListener.memberDisconnected(leaver);
		}
		
		for (int i = 0; i < joiners.length; i++)
		{
			Member joiner = joiners[i];
			introduceTo(joiner);
		}
	}

	/**
	 * The function sends a chunk of data to the receivers.
	 * @param streamingData is the chunk of the media file to be sent.
	 */
	public void sendStreamingMessage(byte[] streamingData) {
		
        Data data = new Data(streamingData, 0, streamingData.length); // Have to provide an offset and length
        integrateMessageType(data, MT_STREAMING);
        sendData(data);
	}
	
	/**
	 * The function sends a notice of a new streaming session to the receivers.
	 * @param fileName is the name of the media file.
	 * @param lengthInKb is the file's length in kilobytes.
	 * @param lengthInSeconds is the file's length in seconds.
	 */
	public void sendNewStreamMessage(String fileName, long lengthInKb, long lengthinSeconds) {
		Data data = new Data();
		integrateNewStreamMessage(data, fileName, lengthInKb, lengthinSeconds);
        integrateMessageType(data, MT_NEW_STREAM);
        sendData(data);
	}
	
	/**
	 * The function sends a message to all receivers asking them to pause the streaming.
	 */
	public void sendPause() {
        Data data = new Data(); // Have to provide an offset and length
        integrateMessageType(data, MT_PAUSE);
        sendData(data);
	}
	
	/**
	 * The function sends a message to all receivers allowing them to continue the streaming.
	 */
	public void sendContinue() {
        Data data = new Data(); // Have to provide an offset and length
        integrateMessageType(data, MT_CONTINUE);
        sendData(data);
	}
	
	/**
	 * The function sends a message to all receivers saying that the current streaming session has ended.
	 */
	public void sendSenderStopped() {
        Data data = new Data(); // Have to provide an offset and length
        integrateMessageType(data, MT_STOPPED);
        sendData(data);
	}
	
	/**
	 * The function sends a message to all receivers saying that the sender is done streaming.
	 */
	public void sendSenderFinished() {
        Data data = new Data(); // Have to provide an offset and length
        integrateMessageType(data, MT_STREAM_FINISHED);
        sendData(data);
	}
	
	/**
	 * The function integrates the information of a new streaming session to the data object.
	 * @param data is the relevant data object.
	 * @param fileName is the name of the media file.
	 * @param lengthInKb is the file's length in kilobytes.
	 * @param lengthInSeconds is the file's length in seconds.
	 */
	private void integrateNewStreamMessage(Data data, String fileName, long lengthInKb, long lengthInSeconds) {
		NewStreamMessage newStream = new NewStreamMessage();
		newStream.setFileName(fileName);
		newStream.setLengthInKb(lengthInKb);
		newStream.setLengthInSeconds(lengthInSeconds);
		data.integrateHeader(newStream, s_NewStreamSerializationAgent);
	}
	
	/**
	 * The function is called when a user quits. Creates a leave notification message and sends it 
	 */
	public void quit()
	{
		Data data = new Data();
		integrateMessageType(data, MT_LEAVE_NOTIFICATION);
		sendData(data);
	}
	
//	/* Received from Alex (testing purposes). Returns the sequence number of the received package */
//    public long exctractMessageSequenceNumber(Data data)
//    {
//        final int OFFSET_TO_NEEDED_HEADER = 9;
//        ac.technion.gcframework.serialization.IObjectSerializationAgent serializationAgent =
//            ac.technion.gcframework.layers.headers.SerializationAgents.getAgent(ac.technion.gcframework.layers.headers.SequencingLayerData.class);
//        ac.technion.gcframework.layers.headers.SequencingLayerData header =
//            (ac.technion.gcframework.layers.headers.SequencingLayerData)serializationAgent.deSerialize
//                (data.getBuffer(), data.getOffset() - OFFSET_TO_NEEDED_HEADER);
//       
//        return header.getSequenceNumber();
//    }
}
